﻿/*************************************
 * Creator:SW-PeiYang
 * DateTime:2023/9/13 9:51:42
 * Description:<Description>
 * CopyRight:中广核研究院有限公司
 * ***********************************/

using Cpjit.SunnyPaine.Toolkit.Net.Sockets.IDataHandlingApdapter;
using Cpjit.SunnyPaine.Toolkit.Net.Sockets.Model;
using Cpjit.SunnyPaine.Toolkit.Net.Sockets.Util;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Cpjit.SunnyPaine.Toolkit.Net.Sockets.UDP.Impl
{
    /// <summary>
    /// UDP通讯。
    /// </summary>
    public class UdpClient : IUdpClient
    {
        #region 私有变量
        /// <summary>
        /// 是否工作中。
        /// </summary>
        private bool isRunning;
        /// <summary>
        /// 在线的远端UDP。键：默认为IP端口；值：Udp客户端会话。
        /// </summary>
        private readonly ConcurrentDictionary<string, SocketSession> onlineRemotes;
        /// <summary>
        /// UDP通讯对象。
        /// </summary>
        //private UdpSession udp;
        private Socket udpClient;
        private readonly int bufferSize = ushort.MaxValue;
        private static readonly EndPoint IPAddressAny = new IPEndPoint(IPAddress.Any, 0);
        #endregion


        #region 公共属性
        /// <summary>
        /// 收到消息后是否自动回复。
        /// </summary>
        public bool IsAutoReply { get; set; }
        /// <summary>
        /// 自动回复的内容。
        /// </summary>
        public string ReplyContent { get; set; } = "SUCCESS";
        /// <summary>
        /// 本机IP。
        /// </summary>
        public string IP { get; private set; }
        /// <summary>
        /// 本机端口。
        /// </summary>
        public int Port { get; private set; }
        /// <summary>
        /// 符合 127.0.0.1:8000 标准格式的IP端口地址。
        /// </summary>
        public string IPHost { get; }
        /// <summary>
        /// 指示 <see cref="ISocket"/> 的实现是客户端还是服务端。true：客户端。false：服务端。
        /// UDP本质上没有客户端与服务端的概念，该属性用于自定义业务性质。
        /// </summary>
        public bool? IsClient { get; private set; }
        /// <summary>
        /// 启用心跳机制。
        /// </summary>
        public bool UseHeartbeat { get; set; }
        /// <summary>
        /// 心跳周期。单位：ms。默认1000ms。
        /// </summary>
        public int HeartbeatInterval { get; set; } = 1000;
        /// <summary>
        /// 编码。
        /// </summary>
        public Encoding Encoder { get; set; } = Encoding.UTF8;
        /// <summary>
        /// 数据处理适配器。
        /// </summary>
        public IDataHandlingAdapter Adapter { get; private set; }
        #endregion


        #region 构造方法
        /// <summary>
        /// 使用默认的参数创建 <see cref="UdpClient"/> 的实例。
        /// </summary>
        public UdpClient()
        { }

        /// <summary>
        /// 使用指定端口参数创建 <see cref="UdpClient"/> 的实例。并以此端口作为终结点端口绑定监听。
        /// </summary>
        /// <param name="port"></param>
        public UdpClient(int port)
        {
            this.Port = port;
        }

        /// <summary>
        /// 使用指定IP和端口参数创建 <see cref="UdpClient"/> 的实例。并以此IP端口作为终结点绑定监听。
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        public UdpClient(string ip, int port)
        {
            this.IP = ip;
            this.Port = port;
            this.IPHost = $"{ip}:{port}";
            this.onlineRemotes = new ConcurrentDictionary<string, SocketSession>();
        }
        #endregion


        #region 事件委托
        /// <summary>
        /// 当连接时发生。
        /// </summary>
        public event Action<IUdpClient, SessionEventArgs> OnConnected;
        /// <summary>
        /// 当断开连接时发生。
        /// </summary>
        public event Action<IUdpClient, SessionEventArgs> OnDisConnected;
        /// <summary>
        /// 当收到消息时发生。
        /// </summary>
        public event Action<IUdpClient, SessionEventArgs> OnReceived;
        /// <summary>
        /// 当出现错误时发生。
        /// </summary>
        public event Action<IUdpClient, SessionEventArgs, Exception> OnError;
        #endregion


        #region 私有方法
        private void SendCallback(IAsyncResult iar)
        {
            try
            {
                if (this.isRunning == false && this.udpClient == null)
                {
                    return;
                }
                this.udpClient.EndSend(iar);
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "接收消息时发生错误。" }, ex);
            }
        }

        private void ReceiveCallback(IAsyncResult iar)
        {
            EndPoint endPoint = IPAddressAny;
            SocketSession session = iar.AsyncState as SocketSession;
            try
            {
                if (this.isRunning == false && this.udpClient == null)
                {
                    return;
                }
                int bytesRead = this.udpClient.EndReceiveFrom(iar, ref endPoint);
                if (bytesRead > 0)
                {
                    string ipHost = endPoint.ToString();
                    session.LastReceiveTime = DateTime.Now;
                    this.UpdateSessionTime(ipHost);
                    byte[] buffer = new byte[bytesRead];
                    Buffer.BlockCopy(session.Buffer, 0, buffer, 0, bytesRead);

                    //判断是否为心跳
                    bool isPing = true;
                    for (int i = 0; i < 4; i++)
                    {
                        isPing = isPing && buffer[i] == byte.MaxValue;
                    }
                    if (isPing)
                    {
                        byte[] bClientId = new byte[bytesRead - 4];
                        Array.Copy(session.Buffer, 4, bClientId, 0, bytesRead - 4);
                        this.AddRemote(endPoint, this.Encoder.GetString(bClientId));
                    }
                    else
                    {
                        this.OnReceived?.Invoke(this, new SessionEventArgs() { Data = buffer, RemoteEndPoint = endPoint });
                    }
                }

                if (this.IsClient == false)
                {
                    endPoint = IPAddressAny;
                }
                this.udpClient.BeginReceiveFrom(session.Buffer, 0, this.bufferSize, SocketFlags.None, ref endPoint,
                    new AsyncCallback(this.ReceiveCallback), session);
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode != SocketError.Success)
                {
                    this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "接收消息时发生错误。" }, ex);
                    this.Dispose();
                }
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "接收消息时发生错误。" }, ex);
                if (this.IsClient == false)
                {
                    endPoint = IPAddressAny;
                }
                this.udpClient?.BeginReceiveFrom(session.Buffer, 0, this.bufferSize, SocketFlags.None, ref endPoint,
                    new AsyncCallback(this.ReceiveCallback), session);
            }
        }

        private void HeartbeatPong()
        {
            Task.Factory.StartNew(() =>
            {
                while (this.isRunning)
                {
                    try
                    {
                        string key = null;
                        foreach (KeyValuePair<string, SocketSession> item in this.onlineRemotes)
                        {
                            if ((DateTime.Now - item.Value.LastReceiveTime).TotalMilliseconds > 5000)
                            {
                                key = item.Key;
                                break;
                            }
                        }
                        if (key != null)
                        {
                            SocketSession tmp;
                            this.onlineRemotes.TryRemove(key, out tmp);
                            this.OnDisConnected?.Invoke(this, new SessionEventArgs()
                            {
                                RemoteEndPoint = new IPEndPoint(IPAddress.Parse(tmp.ClientIP), tmp.ClientPort),
                                SysMsg = "心跳超时"
                            });
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "客户端心跳监测出错。" }, ex);
                    }
                    Thread.Sleep(500);
                }
            });
        }

        private void HeartbeatPing(string serverIP, int serverPort)
        {
            Ping ping = new Ping();
            ByteBlock block = new ByteBlock();
            block.WriteValue(new byte[] { 0XFF, 0XFF, 0XFF, 0XFF });
            block.WriteValue($"{serverIP}:{serverPort}");
            block.EndWrite(false);

            byte[] data = block.ToAarray();
            Task.Factory.StartNew(() =>
            {
                while (this.isRunning)
                {
                    try
                    {
                        PingReply reply = ping.Send(serverIP, 100);
                        if (reply.Status == IPStatus.Success)
                        {
                            this.Send(data);
                        }
                    }
                    catch (Exception ex)
                    {
                        this.OnError?.Invoke(this, new SessionEventArgs(), ex);
                    }
                    Thread.Sleep(this.HeartbeatInterval);
                }
                ping.Dispose();
            });
        }

        private void UpdateSessionTime(string clientId)
        {
            if (this.onlineRemotes.ContainsKey(clientId))
            {
                this.onlineRemotes[clientId].LastReceiveTime = DateTime.Now;
            }
        }
        #endregion


        #region 公共方法，ISocket成员
        /// <summary>
        /// 设置数据处理器适配器。
        /// </summary>
        /// <param name="adapter"></param>
        public void SetDataHandlingAdapter(IDataHandlingAdapter adapter)
        { }
        #endregion


        #region 公共方法，IUdpClient成员
        /// <summary>
        /// 绑定一个本地终结点，作为类服务端，启动UDP通讯。
        /// </summary>
        public bool Start()
        {
            try
            {
                if (this.isRunning)
                {
                    if (this.IsClient == true)
                    {
                        throw new Exception("UDP通讯以已作为客户端启动。");
                    }
                    throw new Exception("UDP通讯已启动。");
                }

                this.udpClient = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                int SIP_UDP_CONNRESET = -1744830452;
                this.udpClient.IOControl(SIP_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null);
                this.udpClient.Bind(new IPEndPoint(IPAddress.Parse(this.IP), this.Port));
                this.IsClient = false;
                //开始接收数据
                EndPoint endPoint = IPAddressAny;
                SocketSession session = new SocketSession("0.0.0.0", 0, this.bufferSize);
                this.udpClient.BeginReceiveFrom(session.Buffer, 0, this.bufferSize, SocketFlags.None, ref endPoint,
                    new AsyncCallback(this.ReceiveCallback), session);

                if (this.UseHeartbeat)
                {
                    this.HeartbeatPong();
                }

                this.isRunning = true;
                return true;
            }
            catch (Exception ex)
            {
                this.Stop();
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "启动UPD通讯出错。" }, ex);
                return false;
            }
        }

        /// <summary>
        /// 作为类客户端，连接到远端UDP。
        /// </summary>
        /// <param name="remoteIP"></param>
        /// <param name="remotePort"></param>
        /// <returns></returns>
        public bool Connect(string remoteIP, int remotePort)
        {
            try
            {
                if (this.isRunning)
                {
                    if (this.IsClient == false)
                    {
                        throw new Exception("UDP通讯以已作为服务端监听启动。");
                    }
                    throw new Exception("UDP通讯已启动。");
                }

                this.udpClient = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                uint IOC_IN = 0x80000000;
                uint IOC_VENDOR = 0x18000000;
                uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
                this.udpClient.IOControl((int)SIO_UDP_CONNRESET, new byte[] { Convert.ToByte(false) }, null);
                this.udpClient.Connect(new IPEndPoint(IPAddress.Parse(remoteIP), remotePort));
                this.IsClient = true;
                //开始接收数据
                EndPoint endPoint = new IPEndPoint(IPAddress.Parse(remoteIP), remotePort);
                SocketSession session = new SocketSession(remoteIP, remotePort, this.bufferSize);
                this.udpClient.BeginReceiveFrom(session.Buffer, 0, this.bufferSize, SocketFlags.None, ref endPoint,
                    new AsyncCallback(this.ReceiveCallback), session);
                Tuple<string, int> tuple = this.udpClient.LocalEndPoint.GetIpPort();
                this.IP = tuple.Item1;
                this.Port = tuple.Item2;

                this.isRunning = true;
                if (this.UseHeartbeat)
                {
                    this.HeartbeatPing(this.IP, this.Port);
                }

                return true;
            }
            catch (Exception ex)
            {
                this.Stop();
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "启动UPD通讯出错。" }, ex);
                return false;
            }
        }

        public void AddRemote(EndPoint endPoint, string clientId)
        {
            if (this.onlineRemotes.ContainsKey(clientId))
            {
                this.onlineRemotes[clientId].LastReceiveTime = DateTime.Now;
                return;
            }
            Tuple<string, int> tuple = endPoint.GetIpPort();
            this.onlineRemotes.TryAdd(clientId, new SocketSession(tuple.Item1, tuple.Item2, this.bufferSize) { ClientId = clientId });
        }

        /// <summary>
        /// 停止和释放UDP通讯。作用同 <see cref="IDisposable.Dispose()"/> 方法。
        /// </summary>
        public void Stop()
        {
            this.Dispose();
        }

        /// <summary>
        /// （连接远端UDP成功后）发送消息。
        /// </summary>
        /// <param name="message">消息内容。</param>
        public void Send(string message)
        {
            byte[] datas = this.Encoder.GetBytes(message);
            this.Send(datas);
        }

        /// <summary>
        /// （连接远端UDP成功后）发送消息。
        /// </summary>
        /// <param name="datas">消息数据。</param>
        public void Send(byte[] datas)
        {
            try
            {
                this.udpClient?.Send(datas);
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "发送消息出错。" }, ex);
            }
        }

        /// <summary>
        /// （连接远端UDP成功后）发送消息。
        /// </summary>
        /// <param name="message">消息内容。</param>
        public void SendAsync(string message)
        {
            byte[] datas = this.Encoder.GetBytes(message);
            this.SendAsync(datas);
        }

        /// <summary>
        /// （连接远端UDP成功后）发送消息。
        /// </summary>
        /// <param name="datas">消息数据。</param>
        public void SendAsync(byte[] datas)
        {
            try
            {
                this.udpClient?.BeginSend(datas, 0, datas.Length, SocketFlags.None,
                    new AsyncCallback(this.SendCallback), null);
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "发送消息出错。" }, ex);
            }
        }

        /// <summary>
        /// 发送消息。
        /// </summary>
        /// <param name="message">消息内容。</param>
        /// <param name="targetIP">目标IP。</param>
        /// <param name="targetPort">目标端口。</param>
        public void SendTo(string message, string targetIP, int targetPort)
        {
            byte[] datas = this.Encoder.GetBytes(message);
            this.SendTo(datas, targetIP, targetPort);
        }

        /// <summary>
        /// 发送消息。
        /// </summary>
        /// <param name="datas">消息数据。</param>
        /// <param name="targetIP">目标IP。</param>
        /// <param name="targetPort">目标端口。</param>
        public void SendTo(byte[] datas, string targetIP, int targetPort)
        {
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(targetIP), targetPort);
            try
            {
                this.udpClient?.SendTo(datas, endPoint);
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "发送消息出错。" }, ex);
            }
        }

        /// <summary>
        /// 发送消息。
        /// </summary>
        /// <param name="message">消息内容。</param>
        /// <param name="targetIP">目标IP。</param>
        /// <param name="targetPort">目标端口。</param>
        public void SendToAsync(string message, string targetIP, int targetPort)
        {
            byte[] datas = this.Encoder.GetBytes(message);
            this.SendToAsync(datas, targetIP, targetPort);
        }

        /// <summary>
        /// 发送消息。
        /// </summary>
        /// <param name="datas">消息数据。</param>
        /// <param name="targetIP">目标IP。</param>
        /// <param name="targetPort">目标端口。</param>
        public void SendToAsync(byte[] datas, string targetIP, int targetPort)
        {
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(targetIP), targetPort);
            try
            {
                this.udpClient?.BeginSendTo(datas, 0, datas.Length, SocketFlags.None, endPoint,
                    new AsyncCallback(this.SendCallback), null);
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "发送消息出错。" }, ex);
            }
        }

        /// <summary>
        /// 停止和释放UDP通讯。作用同 <see cref="IUdpClient.Stop()"/> 方法。
        /// </summary>
        public void Dispose()
        {
            try
            {
                this.isRunning = false;
                this.udpClient?.Shutdown(SocketShutdown.Both);
            }
            catch (Exception ex)
            {
                this.OnError?.Invoke(this, new SessionEventArgs() { SysMsg = "停止和释放UDP通讯时出错。" }, ex);
            }
            finally
            {
                if (this.udpClient != null)
                {
                    this.udpClient.Close(100);
                    this.udpClient.Dispose();
                }
                this.udpClient = null;
            }
        }
        #endregion
    }
}
